from pathlib import Path
import os
import uuid
import shutil
import zipfile
import subprocess
import json
import threading
import urllib.request
import urllib.error
import secrets
from flask import Flask, request, redirect, url_for, send_file, render_template_string, jsonify, Response, session

# 复用现有处理逻辑
from pic_converter import scan_and_process, recolor_id_photo, resize_id_photo


app = Flask(__name__)
# 会话密钥（生产建议使用环境变量 SECRET_KEY）
app.secret_key = os.environ.get("SECRET_KEY", "dev-secret-key")
BASE_DIR = Path.cwd()
JOBS_DIR = BASE_DIR / "web_jobs"
JOBS_DIR.mkdir(parents=True, exist_ok=True)

ALLOWED_EXTENSIONS = {"pdf", "jpg", "jpeg", "png", "zip", "7z", "rar"}

# 简单用户字典与10位随机密码生成（默认生成一个 admin 用户）
def gen_random_password(length: int = 10, digits_only: bool = True) -> str:
    if digits_only:
        alphabet = "0123456789"
    else:
        alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    return "".join(secrets.choice(alphabet) for _ in range(length))

# 从 user/u_p.py（JSON 结构）加载用户列表，形如：
# { "user": [ {"username":"huahua","password":"..."}, ... ] }
def load_users_from_u_p() -> dict:
    try:
        base_dir = Path(__file__).resolve().parent
        cfg_path = base_dir / "user" / "u_p.py"
        with open(cfg_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        users_list = data.get("user") or []
        out = {}
        for it in users_list:
            u = it.get("username")
            p = it.get("password")
            if u and p:
                out[str(u)] = str(p)
        return out
    except Exception as e:
        print(f"加载用户配置失败: {e}")
        return {}

_loaded_users = load_users_from_u_p()
if _loaded_users:
    USERS = _loaded_users
    #print("从 user/u_p.py 加载用户：", ", ".join(USERS.keys()))
else:
    USERS = {"admin": gen_random_password(10, digits_only=True)}
    print(f"初始化登录用户: admin / 密码: {USERS['admin']} (未发现外部用户配置)")

# 登录页面模板（纯表单，无需前端框架）
LOGIN_HTML = """
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登录 | 图片PDF文件压缩转换服务</title>
    <style>
      body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin:0; background:#0f172a; color:#e2e8f0; -webkit-text-size-adjust:100%; }
      *{ box-sizing:border-box; }
      .container { max-width: 420px; margin: 60px auto; padding: 24px; background:#111827; border:1px solid #1f2937; border-radius:12px; box-shadow:0 12px 32px rgba(0,0,0,0.5); }
      h1 { font-size: 20px; margin: 0 0 12px; background: linear-gradient(90deg, #60a5fa, #a78bfa); -webkit-background-clip:text; background-clip:text; color: transparent; }
      label { display:block; font-size:12px; color:#94a3b8; margin-bottom:6px; }
      input { width:100%; background:#1e293b; color:#e2e8f0; border:1px solid #334155; outline:none; font-size:14px; border-radius:8px; padding:10px; }
      input:focus { border-color:#60a5fa; box-shadow:0 0 0 2px #93c5fd; }
      .btn { width:100%; margin-top:12px; background: linear-gradient(90deg, #2563eb, #7c3aed); color:#fff; border:none; padding:10px 14px; border-radius:8px; cursor:pointer; font-weight:700; }
      .sub { color:#9ca3af; font-size:12px; margin-top:10px; }
      .error { color:#ef4444; margin:8px 0; font-size:13px; }
      .hint { font-size:12px; color:#94a3b8; margin-top:8px; }
      @media (max-width: 640px) {
        .container { margin: 24px 12px; padding: 18px; }
        input, .btn { font-size:16px; }
        .btn { padding:12px; }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>登录</h1>
      {% if error %}<div class="error">{{ error }}</div>{% endif %}
      <form method="post" action="/login">
        <div style="margin-bottom:10px">
          <label for="username">用户名</label>
          <input id="username" name="username" placeholder="例如 admin" required />
        </div>
        <div style="margin-bottom:10px">
          <label for="password">密码</label>
          <input id="password" type="password" name="password" placeholder="请输入密码" required />
        </div>
        <button class="btn" type="submit">登录</button>
      </form>
      
    </div>
  </body>
</html>
"""


def allowed_file(filename: str) -> bool:
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


def extract_archive(archive_path: Path, dest_dir: Path) -> bool:
    """解压 zip/7z/rar 到 dest_dir。
    - zip: 使用 Python 内置 zipfile
    - 7z: 使用 py7zr
    - rar: 优先 rarfile（需要系统 UnRAR 或 UnRAR.dll），失败则尝试常见 7-Zip 路径作为回退
    解压成功后会删除源压缩包；失败则保留以便排查。
    """
    ext = archive_path.suffix.lower()
    success = False
    try:
        if ext == ".zip":
            with zipfile.ZipFile(archive_path, "r") as zf:
                zf.extractall(dest_dir)
            success = True
        elif ext == ".7z":
            import py7zr
            with py7zr.SevenZipFile(archive_path, mode="r") as zf:
                zf.extractall(path=dest_dir)
            success = True
        elif ext == ".rar":
            # 1) rarfile 尝试
            try:
                import rarfile
                rf = rarfile.RarFile(archive_path)
                rf.extractall(path=dest_dir)
                rf.close()
                success = True
            except Exception:
                # 2) 尝试 7z：优先 Linux 容器路径与 PATH，其次常见 Windows 安装路径
                candidates = [
                    "/usr/bin/7z",
                    "7z",
                    r"C:\\Program Files\\7-Zip\\7z.exe",
                    r"C:\\Program Files (x86)\\7-Zip\\7z.exe",
                ]
                for exe in candidates:
                    try:
                        subprocess.run([
                            exe, "x", str(archive_path), f"-o{dest_dir}", "-y", "-scsUTF-8"
                        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                        success = True
                        break
                    except Exception:
                        continue
                # 3) 最后尝试 unar（对 RAR5 与中文文件名兼容更好）
                if not success:
                    try:
                        subprocess.run([
                            "unar", "-quiet", "-o", str(dest_dir), str(archive_path)
                        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                        success = True
                    except Exception:
                        pass
        else:
            success = False
    finally:
        if success:
            try:
                archive_path.unlink(missing_ok=True)
            except Exception:
                pass
    if success:
        print(f"[ARCHIVE] 解压 {archive_path.name} -> {dest_dir}")
    else:
        print(f"[ARCHIVE ERROR] 解压失败：{archive_path.name}，请安装 7-Zip 或 UnRAR。")
    return success


INDEX_HTML = """
{% raw %}
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>文件压缩转换服务</title>
    <script src="/vue.js"></script>
    <script>
      // 预览环境CDN可能不可达：提供简易占位以避免报错
      if (!window.Vue) {
        console.warn('Vue CDN 未加载，使用占位以保证页面可预览');
        window.Vue = {
          ref: (v)=>({ value: v }),
          computed: (fn)=>fn(),
          createApp: (opts)=>({
            mount: (sel)=>{
              const el = document.querySelector(sel);
              if (el) {
                el.innerHTML = '<div style="padding:12px;border:1px dashed #334155;border-radius:8px;color:#ef4444;">预览环境未加载 Vue CDN，功能不可用（上传/处理/调整）。但UI已注入占位，您可以在实际环境中访问以体验完整功能。</div>';
              }
            }
          })
        };
      }
    </script>
    <style>
      :root { --bg:#0f172a; --card:#111827; --border:#1f2937; --text:#e2e8f0; --muted:#9ca3af; --hint:#64748b; --accent:#2563eb; --accent2:#7c3aed; }
      *{ box-sizing:border-box; }
      body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans'; margin: 0; background: radial-gradient(1000px 500px at 20% 0%, #0b1020, #0f172a); color: var(--text); }
      .container { max-width: 900px; margin: 0 auto; padding: 32px; }
      .card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 24px; box-shadow: 0 12px 40px rgba(0,0,0,0.45); }
      h1 { font-weight: 800; letter-spacing: 0.4px; font-size: 24px; margin: 0 0 10px; background: linear-gradient(90deg, #60a5fa, #a78bfa); -webkit-background-clip:text; background-clip:text; color: transparent; }
      p.sub { color: var(--muted); margin-top: 0; }
      .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0; }
      .field { background: #0b1220; border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
      label { display:block; font-size: 12px; color:#94a3b8; margin-bottom:6px; }
      /* 亮色化输入与选择框 */
      input[type="number"], select {
        width:100%; background:#1e293b; color:#e2e8f0; border:1px solid #60a5fa;
        outline:none; font-size:14px; border-radius:8px; padding:8px 10px;
      }
      .field :is(input,select):focus { box-shadow: 0 0 0 2px #93c5fd; border-color:#93c5fd; }
      /* 目标大小预设按钮 */
      .presets { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; }
      .preset { background:#0ea5e9; color:#fff; border:none; padding:6px 12px; border-radius:999px; cursor:pointer; font-weight:600; }
      .preset.alt { background:#f59e0b; }
      .preset:hover { filter:brightness(1.08); transform: translateY(-1px); transition: all .15s ease; }
      .drop { border: 2px dashed #334155; border-radius: 12px; padding: 24px; text-align: center; color: #94a3b8; transition: border-color .2s, background .2s; }
      .drop.drag { border-color: #60a5fa; background: rgba(96,165,250,0.08); }
      /* 避免使用 display:none 导致无法触发系统文件选择框 */
      .file-input-hidden { position:absolute; left:-9999px; width:1px; height:1px; opacity:0; }
      .btn { background: linear-gradient(90deg, var(--accent), var(--accent2)); color: white; border: none; padding: 10px 16px; border-radius: 8px; cursor: pointer; font-weight: 700; box-shadow: 0 6px 16px rgba(99,102,241,0.25); }
      .btn:disabled { background: #334155; cursor: not-allowed; }
      .footer { margin-top: 18px; font-size: 12px; color:var(--hint); text-align:center; }
      .success { color:#22c55e; }
      .error { color:#ef4444; }
      /* 功能区分割线 */
      .divider { height: 1px; border: none; margin: 22px 0; background: linear-gradient(90deg, rgba(51,65,85,0), rgba(148,163,184,0.6), rgba(51,65,85,0)); }
      .log { margin-top:12px; background:#0b1220; border:1px solid #1f2937; border-radius:8px; padding:10px; font-size:13px; color:#cbd5e1; max-height:160px; overflow:auto; }
      .badge { display:inline-block; background:#1e293b; border:1px solid #334155; color:#a5b4fc; padding:2px 8px; border-radius:999px; font-size:12px; }
      .file-list { margin-top:10px; list-style:none; padding:0; }
      .file-item { display:flex; align-items:center; justify-content:space-between; padding:6px 10px; border:1px dashed #334155; border-radius:8px; margin-bottom:6px; }
      .remove { background:transparent; border:1px solid #ef4444; color:#ef4444; padding:3px 8px; border-radius:6px; cursor:pointer; font-size:12px; }
      .remove:hover { background:#ef4444; color:#fff; }
      .spinner { display:inline-block; width:16px; height:16px; border:2px solid #93c5fd; border-top-color:transparent; border-radius:50%; animation: spin 0.9s linear infinite; vertical-align:middle; margin-left:6px; }
      @keyframes spin { to { transform: rotate(360deg); } }
      .toast { position:fixed; right:18px; bottom:18px; background:#0b1220; border:1px solid #334155; color:#e2e8f0; padding:10px 14px; border-radius:10px; box-shadow:0 10px 24px rgba(0,0,0,0.35); }
      /* 移动端适配 */
      @media (max-width: 640px) {
        .container { padding: 16px; }
        .card { padding: 16px; }
        .row { grid-template-columns: 1fr; }
        input[type="number"], select { font-size: 16px; }
        .drop { padding: 16px; }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="card">
        <h1>NAS私有-图片PDF文件压缩转换服务</h1>
        <p class="sub">支持 PDF/JPG/JPEG/PNG/ZIP/7Z/RAR · 保留目录结构输出 ZIP</p>
        <div id="app"></div>
      </div>
      <p class="footer">希望每个人都能积极拥抱新生活</p>
    </div>
    <script>
    const V = window.Vue;
    if (!V || !V.createApp) {
      const el = document.getElementById('app');
      if (el) el.innerHTML = '<div style="padding:12px;border:1px dashed #334155;border-radius:8px;color:#ef4444;">前端库未加载（Vue）。请在网络可达的浏览器中访问或稍后重试。</div>';
    } else {
    const { createApp, ref, computed } = V;
    createApp({
      setup(){
        const targetKB = ref(200);
        const outFmt = ref('jpg');
        const files = ref([]);
        const msg = ref('');
        const logs = ref([]);
        const addLog = (t) => { const now = new Date(); const hh = String(now.getHours()).padStart(2,'0'); const mm = String(now.getMinutes()).padStart(2,'0'); logs.value.push(`[${hh}:${mm}] ${t}`); const el = document.getElementById('log'); if (el) el.scrollTop = el.scrollHeight; };
        const pick = () => document.getElementById('file').click();
        const allowedExts = new Set(['pdf','jpg','jpeg','png','zip','7z','rar']);
        const formatSize = (s) => s>=1024*1024 ? (s/1024/1024).toFixed(2)+' MB' : (s/1024).toFixed(1)+' KB';
        const onFiles = (list) => { const arr = Array.from(list || []); const filtered = arr.filter(f=> allowedExts.has((f.name.split('.').pop()||'').toLowerCase())); files.value = filtered; filtered.forEach(f=> addLog('上传：'+f.name)); };
        let poller = null;
        const uploading = ref(false);
        const dragging = ref(false);
        const toast = ref(null);
        const showToast = (text, type='info') => { toast.value = { text, type }; setTimeout(()=>{ toast.value=null; }, 1800); };
        const totalSize = computed(()=> files.value.reduce((acc,f)=> acc + (f.size||0), 0));
        const canUpload = computed(()=> files.value.length>0 && !uploading.value);
        const formatBadge = computed(()=> (outFmt.value||'jpg').toUpperCase());
        const removeFile = (i) => { const f = files.value[i]; files.value.splice(i,1); addLog('移除：'+(f?f.name:'文件')); };
        const handleDrop = (e) => { onFiles(e.dataTransfer.files); dragging.value=false; };
        // 证件照换底色
        const recolorColor = ref('blue');
        const recolorFile = ref(null);
        const recolorUploading = ref(false);
        const recolorPreviewUrl = ref('');
        const recolorMsg = ref('');
        const onRecolorFiles = (list)=>{ const arr = Array.from(list||[]); const f = arr[0]||null; recolorFile.value = f; if (f) addLog('证件照选择：'+f.name); };
        const pickRecolor = ()=>{ const el = document.getElementById('recolorFile'); if (el) el.click(); };
        const recolor = async ()=>{
          if (!recolorFile.value) { recolorMsg.value = '<span class="error">请选择证件照片</span>'; return; }
          const fd = new FormData();
          fd.append('photo', recolorFile.value);
          fd.append('bg_color', recolorColor.value);
          recolorUploading.value = true;
          try{
            const rsp = await fetch('/recolor', { method:'POST', body: fd });
            const data = await rsp.json();
            if (data.error) { recolorMsg.value = '<span class="error">'+data.error+'</span>'; showToast(data.error, 'error'); }
            else { recolorPreviewUrl.value = data.preview_url; recolorMsg.value = `<span class=\"success\">换底色完成。下载：<a href=\"${data.download_url}\" target=\"_blank\">结果图片</a></span>`; showToast('换底色完成', 'success'); }
          }catch(e){ recolorMsg.value = '<span class="error">'+e.toString()+'</span>'; }
          finally{ recolorUploading.value = false; }
        };
        // 证件照尺寸调整
        const resizePreset = ref('一寸证件照');
        const resizeDPI = ref(300);
        const resizeFit = ref('cover');
        const resizeBG = ref('white');
        const resizeFile = ref(null);
        const resizeUploading = ref(false);
        const resizePreviewUrl = ref('');
        const resizeMsg = ref('');
        const resizeWmm = ref(25);
        const resizeHmm = ref(35);
        const onResizeFiles = (list)=>{ const arr = Array.from(list||[]); const f = arr[0]||null; resizeFile.value = f; if (f) addLog('尺寸调整选择：'+f.name); };
        const pickResize = ()=>{ const el = document.getElementById('resizeFile'); if (el) el.click(); };
        const resize = async ()=>{
          if (!resizeFile.value) { resizeMsg.value = '<span class="error">请选择证件照片</span>'; return; }
          const fd = new FormData();
          fd.append('photo', resizeFile.value);
          fd.append('preset', resizePreset.value || '');
          if ((resizePreset.value||'') === '自定义') { fd.append('w_mm', String(resizeWmm.value||25)); fd.append('h_mm', String(resizeHmm.value||35)); }
          fd.append('dpi', String(resizeDPI.value||300));
          fd.append('fit', resizeFit.value||'cover');
          fd.append('bg_color', resizeBG.value||'white');
          resizeUploading.value = true;
          try{
            const rsp = await fetch('/resize', { method:'POST', body: fd });
            const data = await rsp.json();
            if (data.error) { resizeMsg.value = '<span class="error">'+data.error+'</span>'; showToast(data.error, 'error'); }
            else { resizePreviewUrl.value = data.preview_url; resizeMsg.value = `<span class=\"success\">尺寸调整完成。下载：<a href=\"${data.download_url}\" target=\"_blank\">结果图片</a></span>`; showToast('尺寸调整完成', 'success'); }
          }catch(e){ resizeMsg.value = '<span class="error">'+e.toString()+'</span>'; }
          finally{ resizeUploading.value = false; }
        };
        const upload = async () => {
          if (!files.value.length) { msg.value = '<span class="error">请选择文件</span>'; return; }
          const fd = new FormData();
          for (const f of files.value) fd.append('file', f);
          files.value.forEach(f=> addLog('排队：'+f.name));
          fd.append('target_kb', String(targetKB.value || 200));
          fd.append('output_format', outFmt.value || 'jpg');
          msg.value = '上传中...';
          try {
            if (poller) { clearInterval(poller); poller = null; }
            uploading.value = true;
            const rsp = await fetch('/upload', { method: 'POST', body: fd });
            const data = await rsp.json();
            if (data.error) {
              msg.value = '<span class="error">'+data.error+'</span>';
              showToast(data.error, 'error');
              uploading.value = false;
            } else {
              msg.value = '已创建任务，正在处理...';
              const statusUrl = data.status_url;
              poller = setInterval(async ()=>{
                try {
                  const s = await fetch(statusUrl).then(r=>r.json());
                  if (s.phase === 'processing' && s.filename) {
                    addLog('处理：'+s.filename);
                  }
                  if (s.phase === 'error') { clearInterval(poller); poller=null; msg.value = '<span class="error">任务失败：'+(s.message||'未知错误')+'</span>'; showToast((s.message||'任务失败'), 'error'); uploading.value = false; }
                  if (s.phase === 'completed') {
                    clearInterval(poller); poller=null;
                    const dl = s.download_url;
                    msg.value = `<span class=\"success\">处理完成。PDF ${s.pdf_count}，图片 ${s.img_count}，输出 ${s.out_count}。<a href=\"${dl}\" target=\"_blank\">点击这里下载结果</a></span>`;
                    showToast('处理完成', 'success');
                    uploading.value = false;
                  }
                } catch(e) {}
              }, 800);
            }
          } catch(err) { msg.value = '<span class="error">'+err.toString()+'</span>'; showToast(err.toString(), 'error'); uploading.value = false; }
        };
        return { targetKB, outFmt, files, msg, pick, onFiles, upload, logs, totalSize, canUpload, formatBadge, removeFile, formatSize, uploading, dragging, handleDrop, toast,
                 recolorColor, recolorFile, recolorUploading, recolorPreviewUrl, recolorMsg, onRecolorFiles, pickRecolor, recolor,
                 resizePreset, resizeDPI, resizeFit, resizeBG, resizeFile, resizeUploading, resizePreviewUrl, resizeMsg, resizeWmm, resizeHmm, onResizeFiles, pickResize, resize };
      },
      template: `
      <div class="row">
        <div class="field">
          <label>目标大小（KB）</label>
          <input id="targetKB" v-model.number="targetKB" type="number" min="50" max="4096" />
          <div class="presets">
            <button type="button" class="preset" @click="targetKB=100">100KB</button>
            <button type="button" class="preset" @click="targetKB=200">200KB</button>
            <button type="button" class="preset" @click="targetKB=500">500KB</button>
            <button type="button" class="preset alt" @click="targetKB=1024">1MB</button>
          </div>
          <div class="hint">可直接选择预设或输入自定义大小</div>
        </div>
        <div class="field">
          <label>输出格式</label>
          <select id="outFmt" v-model="outFmt">
            <option value="jpg">JPG</option>
            <option value="jpeg">JPEG</option>
            <option value="png">PNG</option>
            <option value="pdf">PDF</option>
          </select>
          <div style="margin-top:8px"><span class="badge">格式：{{ formatBadge }}</span> · <span class="badge">目标：{{ targetKB }}KB</span></div>
        </div>
      </div>
      <div class="drop" id="drop"
           @dragenter.prevent="dragging=true" @dragover.prevent
           @dragleave="dragging=false" @drop.prevent="handleDrop($event)"
           :class="{drag: dragging}"
      >
        拖拽文件到此或 <button class="btn" @click="pick">选择文件</button>
      </div>
      <input type="file" id="file" class="file-input-hidden" multiple accept=".pdf,.jpg,.jpeg,.png,.zip,.7z,.rar" @change="onFiles($event.target.files)" />
      <div style="margin-top:10px; color:#94a3b8">已选择 {{ files.length }} 个文件 · 总计 {{ formatSize(totalSize) }}</div>
      <ul class="file-list">
        <li v-for="(f,i) in files" :key="i" class="file-item">
          <div style="display:flex;gap:8px;align-items:center"><span>{{ f.name }}</span><span class="badge">{{ formatSize(f.size) }}</span></div>
          <button class="remove" @click="removeFile(i)">移除</button>
        </li>
      </ul>
      <div style="margin-top:12px">
        <button class="btn" @click="upload" :disabled="!canUpload">{{ uploading ? '处理中...' : '上传并转换' }}</button>
        <span v-if="uploading" class="spinner"></span>
      </div>
      <div v-html="msg" style="margin-top:12px"></div>
       <div id="log" class="log"><div v-for="l in logs">{{ l }}</div></div>
       <div class="divider"></div>
        <!-- 证件照换底色功能区 -->
        <div class="field" style="margin-top:16px">
          <label>证件照片换底色</label>
          <div class="row">
            <div class="field">
              <label>证件照片</label>
              <div class="drop" @dragenter.prevent="dragging=true" @dragover.prevent @dragleave="dragging=false" @drop.prevent="onRecolorFiles($event.dataTransfer.files)" :class="{drag: dragging}">
                拖拽照片到此或 <button class="btn" type="button" @click="pickRecolor">选择照片</button>
              </div>
              <input type="file" id="recolorFile" class="file-input-hidden" accept=".jpg,.jpeg,.png" @change="onRecolorFiles($event.target.files)" />
              <div style="margin-top:10px; color:#94a3b8">{{ recolorFile ? recolorFile.name : '未选择文件' }}</div>
            </div>
            <div class="field">
              <label>底色</label>
              <select id="bgColor" v-model="recolorColor">
                <option value="blue">蓝底</option>
                <option value="red">红底</option>
                <option value="white">白底</option>
              </select>
            </div>
          </div>
         <div style="margin-top:12px">
           <button class="btn" @click="recolor" :disabled="!recolorFile || recolorUploading">{{ recolorUploading ? '处理中...' : '更换底色' }}</button>
           <span v-if="recolorUploading" class="spinner"></span>
         </div>
         <div v-html="recolorMsg" style="margin-top:12px"></div>
       <div v-if="recolorPreviewUrl" style="margin-top:10px">
         <img :src="recolorPreviewUrl" alt="预览" style="max-width:100%; border:1px solid #1f2937; border-radius:8px;" />
       </div>
       </div>
       <div class="divider"></div>
       <!-- 证件照尺寸调整功能区 -->
       <div class="field" style="margin-top:16px">
         <label>证件照片尺寸调整</label>
         <!-- 将选择照片移动到最前 -->
         <div class="field">
           <label>证件照片</label>
           <div class="drop" @dragenter.prevent="dragging=true" @dragover.prevent @dragleave="dragging=false" @drop.prevent="onResizeFiles($event.dataTransfer.files)" :class="{drag: dragging}">
             拖拽照片到此或 <button class="btn" type="button" @click="pickResize">选择照片</button>
           </div>
           <input type="file" id="resizeFile" class="file-input-hidden" accept=".jpg,.jpeg,.png" @change="onResizeFiles($event.target.files)" />
           <div style="margin-top:10px; color:#94a3b8">{{ resizeFile ? resizeFile.name : '未选择文件' }}</div>
         </div>
         <div class="row" style="margin-top:8px">
           <div class="field">
             <label>预设尺寸</label>
             <select id="resizePreset" v-model="resizePreset">
               <option>一寸证件照</option>
               <option>两寸证件照</option>
               <option>护照照片</option>
               <option>大一寸</option>
               <option>小一寸</option>
               <option>英语四六级</option>
               <option>教师资格证</option>
               <option>国家公务员考试</option>
               <option>导游资格考试</option>
               <option>简历</option>
               <option>学籍照片</option>
               <option>网上报名</option>
               <option>自定义</option>
             </select>
             <div class="presets" style="margin-top:8px">
               <span class="badge">DPI：{{ resizeDPI }}</span>
               <button type="button" class="preset" @click="resizeDPI=300">300</button>
               <button type="button" class="preset" @click="resizeDPI=350">350</button>
               <button type="button" class="preset alt" @click="resizeDPI=600">600</button>
             </div>
           </div>
           <div class="field" v-if="resizePreset==='自定义'">
             <label>自定义尺寸（毫米）</label>
             <div class="row">
               <div class="field">
                 <label>宽(mm)</label>
                 <input type="number" v-model.number="resizeWmm" min="10" max="60" />
               </div>
               <div class="field">
                 <label>高(mm)</label>
                 <input type="number" v-model.number="resizeHmm" min="10" max="80" />
               </div>
             </div>
           </div>
         </div>
         <div class="row" style="margin-top:8px">
           <div class="field">
             <label>适配方式</label>
             <select id="resizeFit" v-model="resizeFit">
               <option value="cover">充满裁剪（无留边）</option>
               <option value="contain">完整适配（居中留边）</option>
             </select>
           </div>
           <div class="field">
             <label>背景颜色（留边时生效）</label>
             <select id="resizeBG" v-model="resizeBG">
               <option value="white">白色</option>
               <option value="blue">蓝色</option>
               <option value="red">红色</option>
             </select>
           </div>
         </div>
         <div style="margin-top:12px">
           <button class="btn" @click="resize" :disabled="!resizeFile || resizeUploading">{{ resizeUploading ? '处理中...' : '调整尺寸' }}</button>
           <span v-if="resizeUploading" class="spinner"></span>
         </div>
         <div v-html="resizeMsg" style="margin-top:12px"></div>
         <div v-if="resizePreviewUrl" style="margin-top:10px">
           <img :src="resizePreviewUrl" alt="预览" style="max-width:100%; border:1px solid #1f2937; border-radius:8px;" />
         </div>
       </div>
       <div v-if="toast" class="toast">{{ toast.text }}</div>
       `
     }).mount('#app');
    }
    </script>
  </body>
</html>
{% endraw %}
"""


RESULT_HTML = """
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>处理完成</title>
    <style>
      body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Microsoft YaHei"; margin: 40px; }
      .card { max-width: 720px; margin: 0 auto; padding: 24px; border: 1px solid #ddd; border-radius: 8px; }
      h1 { font-size: 20px; margin: 0 0 12px; }
      a.button { display: inline-block; background: #52c41a; color: #fff; padding: 10px 18px; border-radius: 6px; text-decoration: none; }
      .path { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: #555; }
    </style>
  </head>
  <body>
    <div class="card">
      <h1>处理完成</h1>
      <p>可下载压缩包：<a class="button" href="{{ url_for('download', job_id=job_id) }}">下载结果 ZIP</a></p>
      <p class="path">任务目录：{{ job_dir }}</p>
    </div>
  </body>
</html>
"""


@app.get("/")
def index():
    # 未登录则跳转登录页
    if not session.get("user"):
        return redirect(url_for("login"))
    return render_template_string(INDEX_HTML)


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "GET":
        # 已登录直接进入首页
        if session.get("user"):
            return redirect(url_for("index"))
        return render_template_string(LOGIN_HTML)
    # POST 登录
    username = (request.form.get("username") or "").strip()
    password = (request.form.get("password") or "").strip()
    if username in USERS and USERS.get(username) == password:
        session["user"] = username
        return redirect(url_for("index"))
    return render_template_string(LOGIN_HTML, error="用户名或密码错误")


@app.get("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


@app.get("/vue.js")
def vue_js():
    # 1) 优先从本地静态文件提供（无需外网）
    local_candidates = [
        BASE_DIR / "static" / "vue.global.prod.js",
        BASE_DIR / "static" / "vue.global.min.js",
        BASE_DIR / "static" / "vue.global.js",
    ]
    for p in local_candidates:
        try:
            if p.exists():
                data = p.read_bytes()
                if data:
                    return Response(data, mimetype="application/javascript")
        except Exception:
            pass
    # 2) 尝试从国内/通用CDN拉取 Vue 全局构建；网络不可达则返回占位，以避免报错
    urls = [
        "https://lib.baomitu.com/vue/3.3.4/vue.global.min.js",
        "https://cdn.staticfile.org/vue/3.3.4/vue.global.min.js",
        "https://unpkg.com/vue@3/dist/vue.global.prod.js",
    ]
    for u in urls:
        try:
            req = urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"})
            with urllib.request.urlopen(req, timeout=4) as resp:
                code = resp.getcode()
                if code == 200:
                    data = resp.read()
                    return Response(data, mimetype="application/javascript")
        except Exception:
            continue
    # 占位：在受限网络环境下避免前端报错；功能不可用，仅展示提示
    placeholder = """
console.warn('Vue CDN 未加载，使用占位以保证页面可预览');
window.Vue = {
  ref: (v) => ({ value: v }),
  computed: (fn) => fn(),
  createApp: (opts) => ({
    mount: (sel) => {
      var el = document.querySelector(sel);
      if (el) {
        el.innerHTML = '<div style="padding:12px;border:1px dashed #334155;border-radius:8px;color:#ef4444;">前端库未加载（Vue）。请在网络可达的浏览器中访问或稍后重试。</div>';
      }
    }
  })
};
"""
    return Response(placeholder, mimetype="application/javascript")


@app.route("/upload", methods=["GET", "POST"])
def upload():
    if request.method == "GET":
        # 兼容直接访问 /upload 的情况，避免 405
        return redirect(url_for("index"))
    # 登录校验：未登录返回 401
    if not session.get("user"):
        return jsonify({"error": "未登录"}), 401
    files = request.files.getlist("file")
    if not files:
        return jsonify({"error": "未选择文件"}), 400

    job_id = uuid.uuid4().hex
    job_dir = JOBS_DIR / job_id
    uploads_dir = job_dir / "uploads"
    uploads_dir.mkdir(parents=True, exist_ok=True)
    # 状态文件路径
    status_file = job_dir / "status.json"

    # 保存文件；压缩包则解压到 uploads_dir
    for f in files:
        if not f or not f.filename:
            continue
        filename = f.filename
        if not allowed_file(filename):
            continue
        ext = filename.rsplit(".", 1)[1].lower()
        if ext in {"zip", "7z", "rar"}:
            tmp_arc = uploads_dir / filename
            f.save(tmp_arc)
            ok = extract_archive(tmp_arc, uploads_dir)
            if not ok:
                # 解压失败则忽略该压缩包
                pass
        else:
            # 直接保存 PDF/JPG/PNG
            save_path = uploads_dir / filename
            f.save(save_path)

    # 读取控制参数
    try:
        target_kb = int(request.form.get("target_kb", "200"))
    except Exception:
        target_kb = 200
    output_format = (request.form.get("output_format", "jpg") or "jpg").lower()
    if output_format not in {"jpg", "jpeg", "png", "pdf"}:
        output_format = "jpg"

    # 后台线程处理，立即返回 job_id + status_url
    def process_job():
        try:
            # 处理：递归处理并输出到 uploads_dir/result，同时写入状态
            pdf_count, img_count, out_count = scan_and_process(uploads_dir, target_kb=target_kb, output_format=output_format, status_file=status_file)
            # 打包输出为 ZIP
            result_dir = uploads_dir / "result"
            zip_path = job_dir / "result.zip"
            if zip_path.exists():
                zip_path.unlink()
            shutil.make_archive(zip_path.with_suffix(""), "zip", root_dir=result_dir)
            # 完成状态
            # 在后台线程中避免 Flask 应用上下文问题，直接构造下载相对路径
            status = {
                "phase": "completed",
                "pdf_count": pdf_count,
                "img_count": img_count,
                "out_count": out_count,
                "download_url": f"/download/{job_id}"
            }
            status_file.write_text(json.dumps(status, ensure_ascii=False), encoding="utf-8")
        except Exception as e:
            try:
                status_file.write_text(json.dumps({"phase": "error", "message": str(e)}, ensure_ascii=False), encoding="utf-8")
            except Exception:
                pass

    # 更新状态为排队/开始
    try:
        status_file.write_text(json.dumps({"phase": "queued"}, ensure_ascii=False), encoding="utf-8")
    except Exception:
        pass
    threading.Thread(target=process_job, daemon=True).start()

    return jsonify({
        "job_id": job_id,
        "status_url": url_for("status", job_id=job_id)
    })


@app.get("/status/<job_id>")
def status(job_id: str):
    # 登录校验：未登录返回错误阶段
    if not session.get("user"):
        return jsonify({"phase": "error", "message": "未登录"}), 401
    job_dir = JOBS_DIR / job_id
    status_file = job_dir / "status.json"
    if not status_file.exists():
        return jsonify({"phase": "queued"})
    try:
        data = json.loads(status_file.read_text(encoding="utf-8") or "{}")
        # 不暴露服务器目录，仅返回处理阶段与必要信息
        safe = {k: v for k, v in data.items() if k in {"phase", "filename", "pdf_count", "img_count", "out_count", "download_url", "message"}}
        return jsonify(safe or {"phase": "queued"})
    except Exception:
        return jsonify({"phase": "queued"})


@app.post("/recolor")
def recolor():
    # 登录校验：未登录返回 401
    if not session.get("user"):
        return jsonify({"error": "未登录"}), 401
    photo = request.files.get("photo")
    if not photo or not photo.filename:
        return jsonify({"error": "未选择证件照片"}), 400
    ext = photo.filename.rsplit(".", 1)[-1].lower()
    if ext not in {"jpg", "jpeg", "png"}:
        return jsonify({"error": "仅支持 JPG/JPEG/PNG"}), 400
    bg_color = (request.form.get("bg_color", "blue") or "blue").lower()
    if bg_color not in {"blue", "red", "white"}:
        bg_color = "blue"

    job_id = uuid.uuid4().hex
    job_dir = JOBS_DIR / job_id
    rec_dir = job_dir / "recolor"
    rec_dir.mkdir(parents=True, exist_ok=True)
    in_path = rec_dir / photo.filename
    photo.save(in_path)
    out_path = rec_dir / "recolor_output.png"
    ok = recolor_id_photo(in_path, bg_color, out_path)
    if not ok or not out_path.exists():
        return jsonify({"error": "换底色失败"}), 500
    return jsonify({
        "job_id": job_id,
        "preview_url": f"/recolor/{job_id}",
        "download_url": f"/recolor/{job_id}?download=1"
    })


@app.get("/recolor/<job_id>")
def recolor_file(job_id: str):
    # 登录校验：未登录跳转登录页
    if not session.get("user"):
        return redirect(url_for("login"))
    out_path = JOBS_DIR / job_id / "recolor" / "recolor_output.png"
    if not out_path.exists():
        return "Not Found", 404
    if request.args.get("download"):
        return send_file(out_path, as_attachment=True, download_name="recolor.png")
    return send_file(out_path)


@app.post("/resize")
def resize():
    # 登录校验：未登录返回 401
    if not session.get("user"):
        return jsonify({"error": "未登录"}), 401
    photo = request.files.get("photo")
    if not photo or not photo.filename:
        return jsonify({"error": "未选择证件照片"}), 400
    ext = photo.filename.rsplit(".", 1)[-1].lower()
    if ext not in {"jpg", "jpeg", "png"}:
        return jsonify({"error": "仅支持 JPG/JPEG/PNG"}), 400

    preset = request.form.get("preset", "") or ""
    try:
        wmm = float(request.form.get("w_mm", "0") or 0)
    except Exception:
        wmm = 0.0
    try:
        hmm = float(request.form.get("h_mm", "0") or 0)
    except Exception:
        hmm = 0.0
    try:
        dpi = int(request.form.get("dpi", "300") or 300)
    except Exception:
        dpi = 300
    fit = (request.form.get("fit", "cover") or "cover").lower()
    if fit not in {"cover", "contain"}:
        fit = "cover"
    bg_color = (request.form.get("bg_color", "white") or "white").lower()
    if bg_color not in {"white", "blue", "red"}:
        bg_color = "white"

    job_id = uuid.uuid4().hex
    job_dir = JOBS_DIR / job_id
    rdir = job_dir / "resize"
    rdir.mkdir(parents=True, exist_ok=True)
    in_path = rdir / photo.filename
    photo.save(in_path)
    out_path = rdir / "resize_output.png"

    preset_arg = preset if (preset and preset != "自定义") else None
    ok = resize_id_photo(in_path, out_path,
                         preset_name=preset_arg,
                         width_mm=wmm or None,
                         height_mm=hmm or None,
                         dpi=dpi,
                         fit=fit,
                         bg_color=bg_color)
    if not ok or not out_path.exists():
        return jsonify({"error": "尺寸调整失败"}), 500
    return jsonify({
        "job_id": job_id,
        "preview_url": f"/resize/{job_id}",
        "download_url": f"/resize/{job_id}?download=1"
    })


@app.get("/resize/<job_id>")
def resize_file(job_id: str):
    # 登录校验：未登录跳转登录页
    if not session.get("user"):
        return redirect(url_for("login"))
    out_path = JOBS_DIR / job_id / "resize" / "resize_output.png"
    if not out_path.exists():
        return "Not Found", 404
    if request.args.get("download"):
        return send_file(out_path, as_attachment=True, download_name="resize.png")
    return send_file(out_path)


@app.get("/download/<job_id>")
def download(job_id: str):
    # 登录校验：未登录跳转登录页
    if not session.get("user"):
        return redirect(url_for("login"))
    zip_path = JOBS_DIR / job_id / "result.zip"
    if not zip_path.exists():
        return "文件不存在或已清理", 404
    return send_file(zip_path, as_attachment=True, download_name=f"result_{job_id}.zip")


if __name__ == "__main__":
    # 启动本地服务（容器中需监听 0.0.0.0）
    app.run(host="0.0.0.0", port=5000, debug=False)

def create_app():
    return app
